﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.IO;
using System.Text.RegularExpressions;
using IGolovkine.ShoutCastRipper.Filters;
using IGolovkine.Core.Diagnostics;
using IGolovkine.Core;

namespace IGolovkine.ShoutCastRipper
{
    public class StationRipper : IStationRipper
    {
        private readonly IStationClient _client;
        private readonly ILogWriter _logWriter;
        private readonly IContentRepository _repository;
        private readonly List<IContentTitleFilter> _titleFilters;
        private readonly IContentTitleProbingPolicy _probingPolicy;
        private readonly IContentLengthPolicy _lengthPolicy;

        public event EventHandler<StationRipperContentTitleChangedEventArgs> ContentTitleChanged;
        public event EventHandler<StationRipperStatusChangedEventArgs> StatusChanged;
        public event EventHandler<StationRipperUrlChangedEventArgs> UrlChanged;
        
        private readonly string _name;

        private StationRipperStatus _status = StationRipperStatus.Stopped;
        private DateTime _currentTitleStartTime = DateTime.MaxValue;
        
        private string _stopAfterCurrentTrack;
        private string _contentTitleToSkip;
        private int _uninterruptedTitleCount;
        private string _currentUrl;
        private string _contentTitle;
        
        public StationRipper(string name, string url, 
            TypeFactory<IStationClient, string> stationFactory,
            IContentRepository contentRepository,
            IContentTitleProbingPolicy probingPolicy,
            IContentLengthPolicy lengthPolicy,
            ILogWriter logWriter)
        {
            Debug.NotNullOrEmpty(() => name);
            Debug.NotDefault(() => stationFactory);
            Debug.NotDefault(() => contentRepository);
            Debug.NotDefault(() => probingPolicy);
            Debug.NotDefault(() => lengthPolicy);
            Debug.NotDefault(() => logWriter);

            _name = name;
            _logWriter = logWriter;
            _lengthPolicy = lengthPolicy;

            _repository = contentRepository;
            
            _client = stationFactory(url);
            _currentUrl = url;

            _probingPolicy = probingPolicy;

            _probingPolicy.ContentTitleProbingDue += new EventHandler<EventArgs>(ProbeContentTitleDue);

            _client.Disconnected += new EventHandler<StationClientDisconnectedEventArgs>(ClientDisconnected);
            _client.ContentStart += new EventHandler<StationClientContentStartEventArgs>(ClientContentStart);
            _client.ContentEnd += new EventHandler<StationClientContentEndEventArgs>(ClientContentEnd);
            _client.ContentReceived += new EventHandler<StationClientContentReceivedEventArgs>(ClientContentDataReceived);

            _titleFilters = new List<IContentTitleFilter>();
        }

        public IList<IContentTitleFilter> TitleFilters
        {
            get { return _titleFilters; }
        }

        public StationRipperStatus Status
        {
            get { return _status; }            
        }

        private void ChangeStatus(StationRipperStatus newStatus, string reason)
        {
            if (_status != newStatus)
            {
                this.OnStatusChanged(new StationRipperStatusChangedEventArgs(_status, newStatus, reason));
                _logWriter.WriteLine("StatusChanged: {0} -> {1}", _status, newStatus);
                _status = newStatus;
            }
        }

        public string Name
        {
            get { return _name; }
        }

        public string CurrentUrl
        {
            get { return _currentUrl; }
        }

        private void ChangeUrl(string newUrl)
        {
            if (_currentUrl != newUrl)
            {
                this.OnUrlChanged(new StationRipperUrlChangedEventArgs(_currentUrl, newUrl));
                _logWriter.WriteLine("UrlChanged: {0} -> {1}", _currentUrl, newUrl);
                _currentUrl = newUrl;
            }
        }

        public string ContentTitle
        {
            get { return _contentTitle; }
        }

        private void ChangeContentTitle(string newContentTitle)
        {
            if (_contentTitle != newContentTitle)
            {
                this.OnContentTitleChanged(new StationRipperContentTitleChangedEventArgs(_contentTitle, newContentTitle));
                _logWriter.WriteLine("ContentTitleChanged: {0} -> {1}", _contentTitle, newContentTitle);
                _contentTitle = newContentTitle;
            }
        }

        public DateTime CurrentContentTitleStartTime
        {
            get { return _currentTitleStartTime; }
        }

        public TimeSpan CurrentContentTitleLength
        {
            get
            {
                if (DateTime.Now - _currentTitleStartTime > TimeSpan.Zero)
                {
                    return DateTime.Now - _currentTitleStartTime;
                }
                else
                {
                    return TimeSpan.Zero;
                }
            }
        }

        public void Dispose()
        {
            this.Stop();
            _client.Dispose();
            _probingPolicy.ContentTitleProbingDue -= new EventHandler<EventArgs>(ProbeContentTitleDue);
            _probingPolicy.Dispose();
        }

        private void ClientContentStart(object sender, StationClientContentStartEventArgs e)
        {
            this.ChangeContentTitle(e.ContentTitle);

            if (_contentTitleToSkip == e.ContentTitle)
            {
                this.SkipCurrentTitle(null);
            }
            else
            {
                _logWriter.WriteLine("Current content title is '{0}'", e.ContentTitle);
                _uninterruptedTitleCount++;
                _currentTitleStartTime = DateTime.Now;
                string reason;
                if (!this.ProcessContentTitleFilters(e.ContentTitle, out reason))
                {
                    this.SkipCurrentTitle(reason);
                }
                else
                {
                    this.ChangeStatus(StationRipperStatus.Ripping, string.Format("Ripping '{0}'", e.ContentTitle));
                    _repository.BeginStoreContent(e.ContentTitle);
                    _logWriter.WriteLine("Ripping '{0}'", e.ContentTitle);
                }
            }
        }

        private void ClientContentEnd(object sender, StationClientContentEndEventArgs e)
        {
            if (_lengthPolicy.WithinMinimumLength(this.CurrentContentTitleLength))
            {
                if (_repository.SavingContent)
                {
                    _repository.EndStoreContent(_uninterruptedTitleCount > 1 && !e.Disconnecting);
                }
            }
            else
            {
                if (_repository.SavingContent)
                {
                    _logWriter.WriteLine("{0} doesn't satisfy minimum length requirement, deleting...", e.ContentTitle);
                    _repository.CancelStoreContent();
                }
            }

            if (_stopAfterCurrentTrack == e.ContentTitle)
            {
                this.Stop();
                _stopAfterCurrentTrack = null;
            }
        }

        private void ClientDisconnected(object sender, StationClientDisconnectedEventArgs e)
        {
            if (e.Exception != null)
            {
                this.ChangeStatus(StationRipperStatus.Disconnected, string.Format("Error: {0}", e.Exception.Message));
                _uninterruptedTitleCount = 0;
                _currentTitleStartTime = DateTime.MaxValue;
                _probingPolicy.ScheduleToReconnect(e.Exception);
                _logWriter.WriteException("Disconnected", e.Exception);
            }
            else if (this.Status == StationRipperStatus.Stopped)
            {
                _uninterruptedTitleCount = 0;
                _currentTitleStartTime = DateTime.MaxValue;
                Debug.Expect(() => e.Exception == null);
            }
            else if (this.Status == StationRipperStatus.Skipping)
            {
                _probingPolicy.ScheduleToReconnect(null);
            } 
        }

        public void SkipCurrentTitle(string reason)
        {
            if (_contentTitleToSkip != _client.CurrentTitle)
            {
                _contentTitleToSkip = _client.CurrentTitle;
                this.ChangeStatus(StationRipperStatus.Skipping, reason); 
                _logWriter.WriteLine("Skipping '{0}'", _client.CurrentTitle);
            }
            _client.Disconnect();
        }

        private bool ProcessContentTitleFilters(string contentTitle, out string reason)
        {
            foreach (IContentTitleFilter filter in _titleFilters)
            {
                ContentTitleFilterResult result = filter.Process(contentTitle);
                if (!result.Accepted)
                {
                    reason = string.Format("'{0}' was blocked by '{1}': {2}", contentTitle, filter.Name, result.Reason);
                    _logWriter.WriteLine(reason);
                    return false;
                }
            }
            reason = null;
            return true;
        }

        private void ClientContentDataReceived(object sender, StationClientContentReceivedEventArgs e)
        {
            if (!_lengthPolicy.WithinMaximumLength(this.CurrentContentTitleLength))
            {
                string reason = string.Format("{0} exceeds maximum length", e.ContentTitle);
                _logWriter.WriteLine(reason);
                this.SkipCurrentTitle(reason);
            }
            else
            {
                _repository.AppendContent(e.Data);
            }
        }

        private void ProbeContentTitleDue(object sender, EventArgs e)
        {
            this.TryConnect();
        }

        private void TryConnect()
        {
            try
            {
                _client.Connect(_currentUrl);
                this.ChangeUrl(_currentUrl);
            }
            catch (Exception ex)
            {
                _logWriter.WriteException("Error connecting", ex);
                _probingPolicy.ScheduleToReconnect(ex);
            }
        }

        public void Start()
        {
            if (this.Status == StationRipperStatus.Stopped)
            {
                this.ChangeStatus(StationRipperStatus.Disconnected, "Waiting to connect");
                _uninterruptedTitleCount = 0; 
                _logWriter.WriteLine("{0} for '{1}' started!", this.GetType().Name, _name);
                MethodHelper.InvokeAsynchronously(() => this.TryConnect());
            }
            else
            {
                throw new InvalidOperationException(string.Format("{0} is already started", this.GetType().Name));
            }
        }

        public void Stop()
        {
            if (this.Status != StationRipperStatus.Stopped)
            {
                if (_client.IsConnected)
                {
                    _client.Disconnect();
                }
                _uninterruptedTitleCount = 0;
                this.ChangeStatus(StationRipperStatus.Stopped, "Stopped");
            }
            else
            {
                throw new InvalidOperationException(string.Format("{0} is already stopped", this.GetType().Name));
            }

            _logWriter.WriteLine("{0} stopped!", this.GetType().Name);
        }

        public void StopAfterCurrentTitle()
        {
            _stopAfterCurrentTrack = _client.CurrentTitle;
        }

        protected virtual void OnStatusChanged(StationRipperStatusChangedEventArgs args)
        {
            if (this.StatusChanged != null)
            {
                this.StatusChanged(this, args);
            }
        }

        protected virtual void OnContentTitleChanged(StationRipperContentTitleChangedEventArgs args)
        {
            if (this.ContentTitleChanged != null)
            {
                this.ContentTitleChanged(this, args);
            }
        }

        protected virtual void OnUrlChanged(StationRipperUrlChangedEventArgs args)
        {
            if (this.UrlChanged != null)
            {
                this.UrlChanged(this, args);
            }
        }

        public bool IsStoppingAfterCurrentTitle
        {
            get { return _stopAfterCurrentTrack == _contentTitle; }
        }

        public void CancelStopAfterCurrentTitle()
        {
            _stopAfterCurrentTrack = null;
        }
    }
}
